from __future__ import annotations
from typing import List
import subprocess
import os 
import shutil
from bcat.util.str import Str
from bcat.process.process import Process
from bcat.error import InvalidArgumentError
from bcat.process.error import CommandNotFoundError

from abc import ABC, abstractmethod
class IExecutable(ABC):
    @abstractmethod
    def directory(self, value: str) -> IExecutable:
        pass
    @abstractmethod
    def binary(self, value: str) -> IExecutable:
        pass
    @abstractmethod
    def args(self, value: str | List[str]) -> IExecutable:
        pass
    @abstractmethod
    def stdout(self, value: bool = True) -> IExecutable:
        pass
    @abstractmethod
    def stderr(self, value: bool = True) -> IExecutable:
        pass
    @abstractmethod
    def stdin(self, value: bool = True) -> IExecutable:
        pass
    @abstractmethod
    def text(self, value: bool = True) -> IExecutable:
        pass

    @abstractmethod
    def get_directory(self) -> str:
        pass
    @abstractmethod
    def get_binary(self) -> str:
        pass
    @abstractmethod
    def get_args(self) -> List[str]:
        pass
    @abstractmethod
    def get_stdout(self) -> bool:
        pass
    @abstractmethod
    def get_stderr(self) -> bool:
        pass
    @abstractmethod
    def get_stdin(self) -> bool:
        pass
    @abstractmethod
    def get_text(self) -> bool:
        pass

    @abstractmethod
    def execute(self) -> Process:
        pass

    @abstractmethod
    def processes(self) -> List[Process]:
        pass


class Executable(IExecutable):
    def __init__(self):
        self._directory = ""
        self._binary = ""
        self._args = []
        self._stdout = False
        self._stdin = False
        self._stderr = False
        self._text = False
        self.__processes = []

    def directory(self, value: str = "") -> IExecutable:
        if self._directory != value:
            self._directory = value
        return self

    def binary(self, value: str) -> IExecutable:
        if Str(value).is_not_empty() and self._binary != value:
            self._binary = value
        return self
    def args(self, value: str | List[str]) -> IExecutable:
        if isinstance(value, str) and Str(value).is_not_empty():
            value = [value,]
            if self._args != value:
                self._args = value
        if isinstance(value, list):
            if self._args != value:
                self._args = value     
        return self
    def stdout(self, value: bool = True) -> IExecutable:
        if value != self._stdout:
            self._stdout = value
        return self
    def stderr(self, value: bool = True) -> IExecutable:
        if value != self._stderr:
            self._stderr = value
        return self
    def stdin(self, value: bool = True) -> IExecutable:
        if value != self._stdin:
            self._stdin = value
        return self
    def text(self, value: bool = True) -> IExecutable:
        if value != self._text:
            self._text = value
        return self

    def get_directory(self) -> str:
        return self._directory
    def get_binary(self) -> str:
        return self._binary
    def get_args(self) -> List[str]:
        return self._args
    def get_stdout(self) -> bool:
        return self._stdout
    def get_stderr(self) -> bool:
        return self._stderr
    def get_stdin(self) -> bool:
        return self._stdin
    def get_text(self) -> bool:
        return self._text

    def execute(self) -> Process:
        if len(self._binary) == 0:
            raise InvalidArgumentError("binary is empty", 0)

        if not isinstance(self._args, List):
            raise InvalidArgumentError("args is not list[str]", 1)

        binary_full_path = self._binary if len(self._binary) == 0 else os.path.join(self._directory, self._binary)
        if shutil.which(binary_full_path) is None:
            raise CommandNotFoundError(binary_full_path, f"execute an cmd error, cmd:[{binary_full_path}]")

        cmdargs = [binary_full_path] + [e for e in self._args]
        
        stdin = subprocess.DEVNULL if not self._stdin else subprocess.PIPE
        stdout = subprocess.DEVNULL if not self._stdout else subprocess.PIPE
        stderr = subprocess.DEVNULL if not self._stderr else subprocess.PIPE

        process = Process(subprocess.Popen(cmdargs, stdin=stdin, stdout=stdout, stderr=stderr, text=self._text))
        self.__processes.append(process)
        return process

    def processes(self) -> List[Process]:
        return self.__processes
        

    def __repr__(self):
        args = " ".join(x for x in self._args)
        binary_full_path = os.path.join(self._directory, self._binary)
        return f"Executable([{binary_full_path} {args}], stdout:{self._stdout},stdin:{self._stdin},stderr:{self._stderr},text:{self._text})"
 


import unittest
class TestExec(unittest.TestCase):
    def test_main(self):
        exe = Executable() \
            .directory("/usr/bin") \
            .binary("python") \
            .args("-v") \
            .stdout() \
            .stdin() \
            .stderr() \
            .text()

        self.assertEqual(exe.get_directory(), "/usr/bin")
        self.assertEqual(exe.get_binary()   , "python")
        self.assertEqual(exe.get_args()     , ["-v",])
        self.assertTrue(exe.get_stdout())
        self.assertTrue(exe.get_stdin())
        self.assertTrue(exe.get_stderr())
        self.assertTrue(exe.get_text())

        exe.execute()


